「你居然又沒有登入了!你又來了!」然後她用拳頭重重的打在我的小腿上。當她想這麼做的時候,她可以打得非常痛。
~節錄自《賴田捕手》第二十一章
到昨天為止,我們利用 flask 和之前學過的 psycopg2 慢慢地幫我們的網頁加入了各式各樣的功能,包括瀏覽所有的訓練資料、調閱符合特定條件的訓練資料、舒適地調閱符合特定條件的訓練資料。實作出了一個從 LINE 聊天機器人輸入資料、在 Heroku Postgres 存放資料,並且在網頁上查詢資料的大平台。但是等等,我們這是在做什麼?這樣一來,我精心且仔細地替草泥馬規劃的訓練菜單不就等於放在網路上供大家任意研究?我當然不介意培養出更多的草泥馬訓練大師,畢竟有更多的人願意投入這個領域我一定很開心。但這個領域也有屬於自己的黑暗面,萬一我的扎實且邏輯的訓練菜單被黑暗草泥馬界的訓練師撿走,那就不好了。這樣發展下去,草泥馬們的未來可是相當堪憂啊。
能不能做一個登入機制,只讓擁有使用權限、對草泥馬充滿著愛的訓練師能夠進入這個網站,並使用這個網站的資源呢?當然沒問題。我們今天就是要來用 flask 的另一個相關套件 Flask-Login 幫我們達成這個目標。
我們從簡單的開始。昨天已經學過要怎麼做出一個可以傳送POST
請求到伺服器的理想表單了。登入頁面當然也是一個表單,使用者必須輸入使用者名稱跟密碼,與我們的資料庫比對,吻合之後才算登入。陽春的登入頁面如下:
<form method="post" action="/login">
<label for="user_id">user_id:</label>
<input type="text" id="user_id" name="user_id">
<label for="password">password:</label>
<input type="password" id="password" name="password">
<button type="submit">登入</button>
</form>
上面的程式碼應該就不用我再多做解釋了,稍微值得一提的是<input type="password">
。昨天有說過,<input>
提供了許多種不同類型的輸入欄位,而我們現在用的這種type="password"
會把使用者輸入的文字藏起來,如圖一中的紅框。
圖一、密碼就該好好藏起來
一樣,再放上一個用 Bootstrap 裝飾過的登入頁面:
<form method="post" action="/login">
<div class="form-group row">
<label for="user_id" class="col-2 offset-1 col-form-label">user_id:</label>
<div class="col-3">
<input type="text" class="form-control" id="user_id" name="user_id" placeholder="user_id">
<small class="form-text">提示: Me</small>
</div>
</div>
<div class="form-group row">
<label for="password" class="col-2 offset-1 col-form-label">password:</label>
<div class="col-3">
<input type="password" class="form-control" id="password" name="password" placeholder="password">
<small class="form-text">提示: myself</small>
</div>
</div>
<div class="form-group row">
<div class="col-2 offset-3">
<button type="submit" class="btn btn-dirty-purple">登入</button>
</div>
</div>
</form>
其實差不多,大同小異,只是多了一些<div class="row">
、<div class="col">
做排版的設計。
這是今天最重要的工作了。我把這個工作分成三部分來介紹➀,第一部分是初始化 Flask-Login,第二部分是登入登出路徑,第三部分是綁定登入限定網址。
和 Jinja2 不同,Flask-Login 並不是和 flask 綁定在一起的,所以我們在將整個檔案推向 Heroku 使用 Flask-Login 之前,記得在requirements.txt
這個檔案裡加入一行:
Flask-Login==0.4.1
請 Heroku 安裝好 Flask-Login 這個套件。
接著就可以回到我們的 Python 檔案:
from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user
app = Flask(__name__)
app.secret_key = config.get('flask', 'secret_key')
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.session_protection = "strong"
login_manager.login_view = 'login'
login_manager.login_message = '請證明你並非來自黑暗草泥馬界'
第一行:from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user
引入我們待會需要用到的方法
第二行: app = Flask(__name__)
這行代表初始化 flask 的程式碼在我們最早用 flask 的時候就寫上去了,現在再提一下,是因為等等要設定一個和 flask 有關的密鑰,並且將 flask 和 Flask-Login 綁定在一起 。
第三行: app.secret_key = config.get('flask', 'secret_key')
設定 flask 的密鑰secret_key
。要先替 flask 設定好secret_key
,Flask-Login 才能運作。secret_key
最好是一串亂碼,而且不要太招搖的讓大家都知道。我們可以用
In [1]: import os
os.urandom(16).hex()
Out[1]: 'dd06be55a06c03312b2ab109b5f8f6ab'
來隨機產生一段適合用來當作secret_key
的亂碼。
第四行:login_manager = LoginManager()
產生一個LoginManager()
物件來初始化 Flask-Login。
第五行: login_manager.init_app(app)
將 flask 和 Flask-Login 綁定起來。
第六行:login_manager.session_protection = "strong"
將session_proctection
調整到最強。預設是"basic"
,也會有一定程度的保護,所以這行可選擇不寫上去。
第七行:login_manager.login_view = 'login'
當使用者還沒登入,卻請求了一個需要登入權限才能觀看的網頁時,我們就先送使用找到login_view
所指定的位置來。以這行程式碼為例,當未登入的使用者請求了一個需要權限的網頁時,就將他送到代表login()
的位址去。我們現在還沒寫出login()
這個函數,所以等等要補上。
第八行:login_manager.login_message = '請證明你並非來自黑暗草泥馬界'
login_message
是和login_view
相關的設定,當未登入的使用者被送到login_view
所指定的位址時,會一併跳出的訊息。
好啦,這樣初始化的步驟就搞定,一半了。另一半:
class User(UserMixin):
pass
@login_manager.user_loader
def user_loader(使用者):
if 使用者 not in users:
return
user = User()
user.id = 使用者
return user
@login_manager.request_loader
def request_loader(request):
使用者 = request.form.get('user_id')
if 使用者 not in users:
return
user = User()
user.id = 使用者
# DO NOT ever store passwords in plaintext and always compare password
# hashes using constant-time comparison!
user.is_authenticated = request.form['password'] == users[使用者]['password']
return user
users = {'Me': {'password': 'myself'}}
先說一下,上面那段程式碼大約做了三件事:初始化 Flask-Login 提供的類別UserMixin
、製作user_loader
、製作request_loader
。
第一行:class User(UserMixin):
宣告我們要借用 Flask-Login 提供的類別UserMixin
,並放在User
這個物件上。其實我們沒有對UserMixin
做出任何更動,因此下面那行程式碼用個pass
就行。
第四行:def user_loader(使用者):
做一個驗證使用者是否登入的user_loader()
。下面的程式碼基本上就是確認使用者
是否是在我們的合法清單users
當中,若沒有,就什麼都不做。若有,就宣告一個我們剛才用UserMixin
做出來的物件User()
,貼上user
標籤,並回傳給呼叫這個函數user_loader()
的地方。
第十一行:def request_loader(request):
做一個從flask.request
驗證使用者是否登入的request_loader()
。下面的程式碼基本上就是確認使用者
是否是在我們的合法清單users
當中,若沒有,就什麼都不做。若有,就宣告一個我們剛才用UserMixin
做出來的物件User()
,貼上user
標籤,並回傳給呼叫這個函數request_loader()
的地方。並在最後利用user.is_authenticated = request.form['password'] == users[使用者]['password']
來設定使用者是否成功登入獲得權限了。若使用者在登入表單中輸入的密碼 request.form['password']
和我們知道的users[使用者]['password']
一樣,就回傳True
到user.is_authenticated
上。
最後一行:users = {'Me': {'password': 'myself'}}
定義一個使用者清單,'Me'
是帳號(或說是使用者名稱),這個帳號的密碼是'myself'
。這當然是一個簡單的設定方式。喜歡的話也可以在 Heroku Postgres 上另外做一個表單(table),並將使用者資料存放在那邊。
既然我們將 Flask-Login 做了基本的設定,接下來就可以開始製作我們先前答應的函數login()
了。
from flask import request, render_template, url_for, redirect, flash
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'GET':
return render_template("login.html")
使用者 = request.form['user_id']
if (使用者 in users) and (request.form['password'] == users[使用者]['password']):
user = User()
user.id = 使用者
login_user(user)
flash(f'{使用者}!歡迎加入草泥馬訓練家的行列!')
return redirect(url_for('from_start'))
flash('登入失敗了...')
return render_template('login.html')
第一行:from flask import request, render_template, url_for, redirect, flash
先確定一下我們已經引入等等會用到的函數了。
第二行:@app.route('/login', methods=['GET', 'POST'])
做一個路由'/login'
,接受GET
跟POST
兩種請求。接下來的程式碼跟昨天的表單有點像:如果使用者送來GET
的請求,就傳回空白表單。
第七行: if (使用者 in users) and (request.form['password'] == users[使用者]['password']):
如果使用者傳來POST
請求,就從使用者提交的表單中拿取資料並檢查是否為合格的使用者。若合格,則用login_user()
幫使用者登入,將使用者引導至頁面return redirect(url_for('from_start'))
,並閃現➁消息flash(f'{使用者}!歡迎加入草泥馬訓練家的行列!')
。若否,將使用者引導至頁面return render_template('login.html')
並閃現消息flash('登入失敗了...')
接著製作一個登出的函數logout()
:
@app.route('/logout')
def logout():
使用者 = current_user.get_id()
logout_user()
flash(f'{使用者}!歡迎下次再來!')
return render_template('login.html')
剩下最簡單的一步了,也就是告訴 Flask-Login 哪些網址需要使用者處於登入狀態才可瀏覽。只要在我們已經做好的路由下面,加上@login_requierd
就搞定,以我們的"/show_records"
為例子:
@app.route("/show_records")
@login_required
def show_records():
python_records =web_select_overall()
return render_template("show_records.html", html_records=python_records)
這樣,當使用者試著瀏覽 "你-APP-的名字.herokuapp.com/show_records" 時,Flask-Login 就會先幫我們檢查使用者是否登入,若否,則將使用者引導至login_manager.login_view
的位址,也就是我們今天做好的登入頁面login()
。
基本上,做完上面那些規劃,我們就可以用登入來限制網頁使用者的身分了,只有登入的使用者才能瀏覽特定頁面。但是我們還有一些小地方可以在修飾一下,讓網頁看起來更完美。怎麼說呢?
圖二、沒登入的使用者還是可以看到完整的瀏覽列
雖然瀏覽列上的部分項目已經透過 Flask-Login 保護住了,必須要登入的使用者才能看到內容,但不管有沒有登入,使用者都可以看到瀏覽列上全部的項目,好像還是有點怪怪的。
這時候我們可以在base.html
這個檔案裡稍作修改:
{% if current_user.is_authenticated %}
<!-- 登入後才可見到的導覽列 -->
{% else %}
<!-- 否則就看到 -->
{% endif %}
強大的 Jinja2 又在此發揮作用了。
第一行:{% if current_user.is_authenticated %}
我們用 Flask-Login 提供的current_user.is_authenticated
來界定使用者是否登入。若有,則可看見下方的物件。
第二行:{% else %}
若否,則看到下方物件。
舉個例子來說:
{% if current_user.is_authenticated %}
<a class="nav-link" href="/logout">登出</a>
{% else %}
<a class="nav-link" href="/login">登入</a>
{% endif %}
若使用者已經登入了,那會看到引導至登出("/logout")的連結。若使用者還沒登入,則會看到引導至登入("/login")的連結。我們只要再對<nav>
其中某些物件做些相同的處理就行囉!
圖三、沒登入的使用者就不該看見所有瀏覽列
好啦,今天我們成功的利用 Flask-Login 做出了使用者登入界面,使用者必須要經過驗證才能夠瀏覽某些網頁。這樣就能保護草泥馬們免於黑暗草泥馬界的荼毒了,太好了!今天相關的程式碼我會放在 Github 上,大家有興趣的可以過去參考看看。另外,網路上也有幾篇寫的蠻好的 Flask-Login 使用心得,我放在下面的參考資料裡。如果覺得今天的內容講的不夠清楚了,可以交互參考,一起研究一下,當然也更歡迎大家在下面留言,我會盡可能的回覆大家的。謝謝!!
➀ Flask-Login Github 官方說明文件
➁ Flask 閃現訊息 flash 官方說明文件
➂ Flask-Login 使用心得
註:對於此系列文有興趣的讀者,歡迎參考由此系列文擴編成書的 LINE Bot by Python,以及最新的系列文《賴田捕手:追加篇》
第 31 天 初始化 LINE BOT on Heroku
第 32 天 快速回覆 QuickReply 介紹
第 33 天 妥善運用 Heroku APP 暫存空間
第 34 天 妥善運用 LINE Notify 免費推播
第 35 天 製造 Deploy to Heroku 按鈕